using System;
using System.Diagnostics;

namespace Atomic.Libraries.Mathematics
{
	/// <summary>
	/// Represents a matrix in the 3-dimensional space.
	/// </summary>
	[Serializable]
	[DebuggerStepThrough]
	[DebuggerDisplay("{DebuggerDisplay}")]
	public sealed class SpaceMatrix
	{
		private Matrix inner;
		[NonSerialized] SpaceVector a1, a2, a3;

		static SpaceMatrix()
		{
			Zero = new SpaceMatrix(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0);
			Identity = new SpaceMatrix(1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0);
		}

		/// <summary>
		/// Creates a new matrix instance.
		/// </summary>
		public SpaceMatrix(double a11, double a12, double a13, double a21, double a22, double a23, double a31, double a32, double a33)
			: this(new Matrix(new double[,] {{ a11, a12, a13 }, { a21, a22, a23 }, { a31, a32, a33 }}))
		{
		}

		/// <summary>
		/// Creates a new matrix instance with column vectors given.
		/// </summary>
		public SpaceMatrix(SpaceVector a1, SpaceVector a2, SpaceVector a3)
			: this(a1.X, a2.X, a3.X, a1.Y, a2.Y, a3.Y, a1.Z, a2.Z, a3.Z) 
		{
		}

		public SpaceMatrix(double[,] entries)
			: this(new Matrix(entries))
		{
			if (entries.GetLength(0) != 3 || entries.GetLength(1) != 3)
			{
				throw new ArgumentException("Invalid size.");
			}
		}

		private SpaceMatrix(Matrix inner)
		{
			this.inner = inner;
		}

		public override string ToString()
		{
			//return string.Format("({0}, {1}, {2})", A1.ToString(), A2.ToString(), A3.ToString());
			return inner.ToString();
		}

		public bool Equals(SpaceMatrix other)
		{
			if (object.ReferenceEquals(other, null))
			{
				return false;
			}

			return inner == other.inner;
		}

		public override bool Equals(object other)
		{
			return Equals(other as SpaceMatrix);
		}
		
		public override int GetHashCode()
		{
			return inner.GetHashCode();
		}

		public static SpaceMatrix Transpose(SpaceMatrix a)
		{
			return new SpaceMatrix(Matrix.Transpose(a));
		}

		public static double Norm(SpaceMatrix a)
		{
			return Matrix.Norm(a.inner);
		}

		public static SpaceMatrix Inverse(SpaceMatrix a)
		{
			SpaceMatrix ainv;
			if (!TryInverse(a, out ainv))
			{
				throw new ArithmeticException("The matrix is singular.");
			}

			return ainv;
		}

		public static bool TryInverse(SpaceMatrix a, out SpaceMatrix ainv)
		{
			double adet;
			InverseDeterminant(a, out ainv, out adet);

			return ainv != null;
		}

		public static double Determinant(SpaceMatrix a)
		{
			SpaceMatrix ainv;
			double adet;
			InverseDeterminant(a, out ainv, out adet);

			return adet;
		}

		private static void InverseDeterminant(SpaceMatrix a, out SpaceMatrix ainv, out double adet)
		{
			double a11 = a[0, 0];
			double a12 = a[0, 1];
			double a13 = a[0, 2];
			double a21 = a[1, 0];
			double a22 = a[1, 1];
			double a23 = a[1, 2];
			double a31 = a[2, 0];
			double a32 = a[2, 1];
			double a33 = a[2, 2];

			// Using http://mathworld.wolfram.com/MatrixInverse.html.
			adet = a11 * a22 * a33 + a12 * a23 * a31 + a13 * a21 * a32 - a11 * a23 * a32 - a12 * a21 * a33 - a13 * a22 * a31;
			if (adet != 0.0)
			{
				ainv = new SpaceMatrix(new Matrix(new double[,] {
					{ (a22 * a33 - a23 * a32) / adet, (a13 * a32 - a12 * a33) / adet, (a12 * a23 - a13 * a22) / adet },
					{ (a23 * a31 - a21 * a33) / adet, (a11 * a33 - a13 * a31) / adet, (a13 * a21 - a11 * a23) / adet },
					{ (a21 * a32 - a22 * a31) / adet, (a12 * a31 - a11 * a32) / adet, (a11 * a22 - a12 * a21) / adet }
				}));
			}
			else
			{
				ainv = null;
			}
		}

		public static SpaceMatrix operator +(SpaceMatrix a, SpaceMatrix b)
		{
			return new SpaceMatrix(a.inner + b.inner);
		}

		public static SpaceMatrix operator +(SpaceMatrix a, double t)
		{
			return new SpaceMatrix(a.inner + t);
		}

		public static SpaceMatrix operator +(double t, SpaceMatrix a)
		{
			return new SpaceMatrix(t + a.inner);
		}

		public static SpaceMatrix operator -(SpaceMatrix a)
		{
			return new SpaceMatrix(-a.inner);
		}

		public static SpaceMatrix operator -(SpaceMatrix a, SpaceMatrix b)
		{
			return new SpaceMatrix(a.inner - b.inner);
		}

		public static SpaceMatrix operator -(SpaceMatrix a, double t)
		{
			return new SpaceMatrix(a.inner - t);
		}

		public static SpaceMatrix operator -(double t, SpaceMatrix a)
		{
			return new SpaceMatrix(t - a.inner);
		}

		public static SpaceMatrix operator *(SpaceMatrix a, SpaceMatrix b)
		{
			return new SpaceMatrix(a.inner * b.inner);
		}

		public static SpaceMatrix operator *(SpaceMatrix a, double t)
		{
			return new SpaceMatrix(a.inner * t);
		}

		public static SpaceMatrix operator *(double t, SpaceMatrix a)
		{
			return new SpaceMatrix(t * a.inner);
		}

		public static SpaceMatrix operator /(SpaceMatrix a, double t)
		{
			return new SpaceMatrix(a.inner / t);
		}

		public static bool operator ==(SpaceMatrix a, SpaceMatrix b)
		{
			if (object.ReferenceEquals(a, null))
			{
				if (object.ReferenceEquals(b, null))
				{
					return true;
				}

				return false;
			}

			return a.Equals(b);
		}

		public static bool operator !=(SpaceMatrix a, SpaceMatrix b)
		{
			return !(a == b);
		}

		public static implicit operator Matrix(SpaceMatrix a)
		{
			return a.inner;
		}

		public static explicit operator SpaceMatrix(Matrix a)
		{
			if (a.Rows != 3 || a.Columns != 3)
			{
				throw new InvalidCastException();
			}

			return new SpaceMatrix(a);
		}

		public static SpaceMatrix Zero
		{
			get;
			private set;
		}

		public static SpaceMatrix Identity
		{
			get;
			private set;
		}

		public double this[int row, int column]
		{
			get
			{
				return inner[row, column];
			}
		}

		/// <summary>
		/// First column vector.
		/// </summary>
		public SpaceVector A1
		{
			get
			{
				if (a1 == null)
				{
					a1 = new SpaceVector(inner[0, 0], inner[1, 0], inner[2, 0]);
				}

				return a1;
			}
		}

		/// <summary>
		/// Second column vector.
		/// </summary>
		public SpaceVector A2
		{
			get
			{
				if (a2 == null)
				{
					a2 = new SpaceVector(inner[0, 1], inner[1, 1], inner[2, 1]);
				}

				return a2;
			}
		}

		/// <summary>
		/// Third column vector.
		/// </summary>
		public SpaceVector A3
		{
			get
			{
				if (a3 == null)
				{
					a3 = new SpaceVector(inner[0, 2], inner[1, 2], inner[2, 2]);
				}

				return a3;
			}
		}

		private string DebuggerDisplay
		{
			get
			{
				return ToString();
			}
		}
	}
}
